﻿using System;
using System.Linq;

using mvcForum.Core;
using mvcForum.Core.Abstractions.Interfaces;
using mvcForum.Core.Events;
using mvcForum.Core.Interfaces.AddOns;
using mvcForum.Core.Interfaces.Events;
using SimpleLocalisation;
using mvcForum.Core.Interfaces.Data;
using mvcForum.Core.Interfaces.Services;

namespace mvcForum.AddOns.Akismet {

	/// <summary>
	/// The Akismet add-on, an anti-spam add-on that fights spam but checking posts/topics against the Akismet "web-service".
	/// </summary>
	public class AkismetAddOn : IAntiSpamAddOn, IAsyncEventListener<NewPostEvent>, IAsyncEventListener<NewTopicEvent>, IAsyncEventListener<PostUpdatedEvent>, IAsyncEventListener<TopicUpdatedEvent> {
		private readonly IConfiguration config;
		private readonly AkismetConfiguration addOnConfig;
		private readonly ILogger logger;
		private readonly IContext context;
		private readonly IRepository<Post> postRepo;
		private readonly IRepository<Topic> topicRepo;
		private readonly IAsyncTask task;
		private readonly TextManager textManager;

		public AkismetAddOn(AkismetConfiguration addOnConfig, IConfiguration config, ILogger logger, IContext context, IRepository<Post> postRepo, IRepository<Topic> topicRepo, IAsyncTask task, TextManager textManager) {
			this.config = config;
			this.addOnConfig = addOnConfig;
			this.logger = logger;
			this.context = context;
			this.postRepo = postRepo;
			this.topicRepo = topicRepo;
			this.task = task;
			this.textManager = textManager;
		}

		#region Queue methods
		/// <summary>
		/// En-queue the new topic event for asynchronously handling!
		/// </summary>
		/// <param name="payload">The event data.</param>
		public void Queue(NewTopicEvent payload) {
			this.Enqueue(payload);
		}

		/// <summary>
		/// En-queue the new post event for asynchronously handling!
		/// </summary>
		/// <param name="payload">The event data.</param>
		public void Queue(NewPostEvent payload) {
			this.Enqueue(payload);
		}

		/// <summary>
		/// En-queue the updated topic event for asynchronously handling!
		/// </summary>
		/// <param name="payload">The event data.</param>
		public void Queue(TopicUpdatedEvent payload) {
			this.Enqueue(payload);
		}

		/// <summary>
		/// En-queue the updated post event for asynchronously handling!
		/// </summary>
		/// <param name="payload">The event data.</param>
		public void Queue(PostUpdatedEvent payload) {
			this.Enqueue(payload);
		}

		private void Enqueue(Object payload) {
			if (this.addOnConfig.Enabled) {
				task.Execute(this, payload, this.addOnConfig.Delay);
			}
		}
		#endregion

		#region Event handlers
		public void Handle(Object payload) {
			if (this.addOnConfig.Enabled) {
				if (payload is TopicUpdatedEvent) {
					this.Handle((TopicUpdatedEvent)payload);
				}
				else if (payload is PostUpdatedEvent) {
					this.Handle((PostUpdatedEvent)payload);
				}
				else if (payload is NewTopicEvent) {
					this.Handle((NewTopicEvent)payload);
				}
				else if (payload is NewPostEvent) {
					this.Handle((NewPostEvent)payload);
				}
				else {
					throw new ApplicationException("Unknown payload!");
				}
			}
		}

		/// <summary>
		/// Handle the updated topic event.
		/// </summary>
		/// <param name="payload">The event data.</param>
		public void Handle(TopicUpdatedEvent payload) {
			if (this.addOnConfig.Enabled) {
				Topic topic = this.topicRepo.Read(payload.TopicId);
				// Not already deleted or quarantined? Should the updated topic be put in quarantine/marked as spam?
				if ((topic.Flag & TopicFlag.Deleted) == 0 &&
					(topic.Flag & TopicFlag.Quarantined) == 0 &&
					this.IsSpam(topic.Posts.OrderBy(p => p.Posted).First(), payload.UserAgent)) {

					// Put this in quarantine right away??
					if (this.addOnConfig.MarkAsSpamOnHit) {
						// Yes, set the  Quarantined flag!
						topic.SetFlag(TopicFlag.Quarantined);
						this.MarkAsSpam(topic.Posts.OrderBy(p => p.Posted).First(), payload);
					}
					else {
						// No, what spam score should we "award" this topic?
						if (this.addOnConfig.SpamScore > 0) {
							// More then zero, let's report the topic as spam!
							topic.ReportSpam(this.addOnConfig.SpamScore);
						}
					}
					this.context.SaveChanges();
				}
			}
		}

		/// <summary>
		/// Handle the updated post event.
		/// </summary>
		/// <param name="payload">The event data.</param>
		public void Handle(PostUpdatedEvent payload) {
			if (this.addOnConfig.Enabled) {
				Post post = this.postRepo.Read(payload.PostId);
				// Not already deleted or quarantined? Should the updated post be put in quarantine/marked as spam?
				if ((post.Flag & PostFlag.Deleted) == 0 &&
					(post.Flag & PostFlag.Quarantined) == 0 &&
					this.IsSpam(post, payload.UserAgent)) {

					// Put this in quarantine right away??
					if (this.addOnConfig.MarkAsSpamOnHit) {
						// Yes, set the Quarantined flag!
						post.SetFlag(PostFlag.Quarantined);

						this.MarkAsSpam(post, payload);
					}
					else {
						// No, what spam score should we "award" this post?
						if (this.addOnConfig.SpamScore > 0) {
							// More than zero, let's report the post as spam!
							post.ReportSpam(this.addOnConfig.SpamScore);
						}
					}
					this.context.SaveChanges();
				}
			}
		}

		/// <summary>
		/// Handle the new post event.
		/// </summary>
		/// <param name="payload">The event data.</param>
		public void Handle(NewPostEvent payload) {
			if (this.addOnConfig.Enabled) {
				// Let's get the post!
				Post post = this.postRepo.Read(payload.PostId);
				// Not already deleted or quarantined? Should the new post be put in quarantine/marked as spam?
				if ((post.Flag & PostFlag.Deleted) == 0 &&
					(post.Flag & PostFlag.Quarantined) == 0 &&
					this.IsSpam(post, payload.UserAgent)) {

					// Put this in quarantine right away??
					if (this.addOnConfig.MarkAsSpamOnHit) {
						// Yes, set the Quaratined flag!
						post.SetFlag(PostFlag.Quarantined);
						
						this.MarkAsSpam(post, payload);
					}
					else {
						// No, what spam score should we "award" this post?
						if (this.addOnConfig.SpamScore > 0) {
							// More than zero, let's report the post as spam!
							post.ReportSpam(this.addOnConfig.SpamScore);
						}
					}
					this.context.SaveChanges();
				}
			}
		}

		/// <summary>
		/// Handle the new topic event.
		/// </summary>
		/// <param name="payload">The event data.</param>
		public void Handle(NewTopicEvent payload) {
			if (this.addOnConfig.Enabled) {
				// Let's get the topic!
				Topic topic = this.topicRepo.Read(payload.TopicId);
				// Not already deleted or quarantined? Should the new topic be put in quarantine/marked as spam?
				if ((topic.Flag & TopicFlag.Deleted) == 0 &&
					(topic.Flag & TopicFlag.Quarantined) == 0 &&
					this.IsSpam(topic.Posts.OrderBy(p => p.Posted).First(), payload.UserAgent)) {

					// Put this in quarantine right away??
					if (this.addOnConfig.MarkAsSpamOnHit) {
						// Yes, set the  Quarantined flag!
						topic.SetFlag(TopicFlag.Quarantined);
						this.MarkAsSpam(topic.Posts.OrderBy(p => p.Posted).First(), payload);
					}
					else {
						// No, what spam score should we "award" this topic?
						if (this.addOnConfig.SpamScore > 0) {
							// More then zero, let's report the topic as spam!
							topic.ReportSpam(this.addOnConfig.SpamScore);
						}
					}
					this.context.SaveChanges();
				}
			}
		}
		#endregion

		private void MarkAsSpam(Post post, InformationBearingEvent payload) {
			post.EditReason += Environment.NewLine + this.textManager.Get("QuarantineReason", ns: "mvcForum.AddOns.Akismet");

			// TODO: Test this null!
			payload.AddFeedback("QuarantineFeedback", "mvcForum.AddOns.Akismet", null);
		}

		/// <summary>
		/// Method for deciding whether or not the post is spam.
		/// </summary>
		/// <param name="post">The post to check against the Akismet web-service.</param>
		/// <returns>True if Akismet reports the content as spam, otherwise false.</returns>
		private Boolean IsSpam(Post post, String userAgent) {
			// Is this event enabled?
			if (this.addOnConfig.Enabled) {
				// Enabled, let's fire up the Akismet service.
				Joel.Net.Akismet kis = new Joel.Net.Akismet(this.addOnConfig.Key, this.config.SiteURL, "MVC Forum");
				if (kis.VerifyKey()) {
					// We have a valid key!
					Joel.Net.AkismetComment comment = new Joel.Net.AkismetComment();
					// TODO: author = "viagra-test-123" to get a positive/negative reply!
					comment.CommentAuthor = post.AuthorName;
					comment.CommentAuthorEmail = post.Author.EmailAddress;
					// TODO: Strip formatting !?!??!?!
					comment.CommentContent = post.Body;

					comment.Blog = this.config.SiteURL;

					comment.CommentType = "comment";
					comment.UserIp = post.IP;
					comment.UserAgent = userAgent;

					// Check the content!!
					return kis.CommentCheck(comment);
				}
				else {
					this.logger.Log(EventType.Error, String.Format("The Akismet API key '{0}' was reported as not valid!", this.addOnConfig.Key));
				}
			}
			return false;
		}

		public Boolean RunAsynchronously {
			get {
				return false;
			}
		}

		public Byte Priority {
			get {
				return (Byte)EventPriority.High;
			}
		}

		public Boolean UniqueEvent {
			get {
				return false;
			}
		}
	}
}